# -*- coding: utf-8 -*-
#import copy
import sys

class ConnectionException(Exception):
    def __init__(self,connection,msg):
        self.msg = msg
        self.connection = connection
        
class ValidateException(Exception):
    def __init__(self,obj,msg):
        self.msg = msg
        self.obj = obj        
        


class Connection(object):
    """ Connection have two fields input and output,
        signal goes from output -> input, so output is input from connection view and
        input if output of connection :)"""
    def __init__(self,invert = False,name = None):
        self.input = None
        self.output = None
        self.value = None
        self.invert = invert
        self.name = name
        self.circuit = None
                
    def setInput(self,gate):
        """ Register input of conneciton """
        realInput = self._addInput(gate)
        if(gate != None):
            gate._addOutput(realInput)

    def setOutput(self,gate):
        """ Register output of conneciton """
        realOutput = self._addOutput(gate)
        if(gate != None):
            gate._addInput(realOutput)
            
    def getGateOutput(self):
        outp = self
        while(isinstance(outp.output,Connection)):
            outp = outp.output
        return outp
    
    def getGateInput(self):
        inp = self
        while(isinstance(inp.input,Connection)):
            inp = inp.input
        return inp
            
    def _addOutput(self,outp):
        """ Called by other connection, do not call explicit """    
        if(self.output != None):
            if(isinstance(self.output,Connection)):
                return self.output._addOutput(outp)
            else:
                raise ConnectionException(self,"Connection Output is already used!")
        self.output = outp
        return self
        
    def _addInput(self,inp):
        """ Called by other connection, do not call explicit """
        if(self.input != None):
            if(isinstance(self.input,Connection)):
                return self.input._addInput(inp)
            else:
                raise ConnectionException(self,"Connection input is already used!")
        self.input = inp
        return self
            
    def setValue(self,val):
        if(val != None):
            self.value = val ^ self.invert
        else:
            self.value = None

    def _repr(self):
        return self.name

    def connect(self,fron,to):
        if(fron != None):
            self.setInput(fron)
        if(to != None):
            self.setOutput(to)
                    
    @staticmethod
    def connectList(fromList,toList):
        if len(fromList) != len(toList):
            raise Exception("List size doesn't match")
        
        for fron,to in zip(fromList,toList):
            fron.setOutput(to)
            

class Gate(object):    
    def __init__(self):
        self.outputList = []
        self.inputList = []
        
    def _addOutput(self, connection):
        self.outputList.append(connection)

    def _addInput(self, connection):
        self.inputList.append(connection)
        
    def removeInput(self,conn):
        self.inputList.remove(conn)
        
    def removeOutput(self,conn):
        self.outputList.remove(conn)
        
    def setOutput(self,connection):
        connection.setInput(self)
        
    def setInput(self,connection):
        connection.setOutput(self)

    def setOutputValue(self,value):
        for output in self.outputList:
            output.setValue(value)
        
    def validate(self):
        pass

    def computeOutput(self):
        pass
    
class Multioutput(Gate):
    def computeOutput(self):
        if(self.outputList[0].value != None):
            return True
        
        if(self.inputList[0].value != None):
            self.setOutputValue(self.inputList[0].value)            

class AndGate(Gate):
    def computeOutput(self):
        if(self.outputList[0].value != None):
            return True
        
        for input in self.inputList:
            if(input.value == None):
                return False
            if(input.value == False):
                self.setOutputValue(False)        
                return True

        self.setOutputValue(True)        
        return True
    
class OrGate(Gate):
    def computeOutput(self):
        if(self.outputList[0].value != None):
            return True
        
        for input in self.inputList:
            if(input.value == None):
                return False
            if(input.value == True):
                self.setOutputValue(True)        
                return True

        self.setOutputValue(False)        
        return True

class XorGate(Gate):
    def computeOutput(self):
        out = self.inputList[0]
        for input in self.inputList[1:]:
            if(input.value == None):
                return False
            out = out ^ input.value
            
        self.setOutputValue(out)        
        return True


class Circuit(object):
    def __init__(self, name=""):
        self.gateList = set()
        self.connectionList = set()
        self.name = name

    def addGate(self,gate):
        self.gateList.add(gate)
        gate.circuit = self
        return gate

    def connect(self,fron,to,invert = False,name = None):
        conn = Connection(invert,name)
        conn.connect(fron,to)
        self.connectionList.add(conn)
        return conn
    
    def disconnect(self,fron,to):
        for outp in fron.outputList:
            a = outp
            while isinstance(a.output,Connection):
                a = a.output
            if(a.output != to):
                continue
            
            fron.removeOutput(outp)
            self.connectionList.remove(outp)
            
            while isinstance(outp.output,Connection):
                self.connectionList.remove(outp.output)
                outp = outp.output
                
            to.removeInput(outp)
            return
            
                                            
    def embedCircuit(self,circuit):    	
        self.connectionList.update(circuit.connectionList)
        self.gateList.update(circuit.gateList)
        circuit.connectionList = None
        
                
                                
class IOCircuit(Circuit):
    def __init__(self,name):
        Circuit.__init__(self,name)
        self.inputList = []
        self.outputList = []
                
    def execute(self,inputVector):
        for conn in self.outputList + list(self.connectionList):
            conn.setValue(None)
            
        for input, value in zip(self.inputList,inputVector):
            input.setValue(value)
            
        while(True):
            end = True
            for output in self.outputList:
                if(output.value == None):
                    end = False
                    break
                    
            if end:
                    break
            
            for gate in self.gateList:
                gate.computeOutput()
           
        ret = []            
        for outp in self.outputList:
            ret.append(outp.value)
        print "Output:" + str(ret)
        return ret

    def createInput(self,name,gateIn):
        conn = Connection(name = name)
        conn.connect(None,gateIn)
        self.inputList.append(conn)
        return conn

    def createOutput(self,name,gateOut):
        conn = Connection(name = name)
        conn.connect(gateOut,None)
        self.outputList.append(conn)
        return conn
    
    def embedCircuit(self,circuit):  
        if(isinstance(circuit,Circuit)):
            Circuit.embedCircuit(self,circuit)
        if(isinstance(circuit,IOCircuit)):
            self.connectionList.update(circuit.inputList)
            self.connectionList.update(circuit.outputList)      
        

    def reduceConnections(self):        
        removedSize = 0
        for connection in list(self.connectionList) + self.inputList:
            if not isinstance(connection.output,Connection) :
                continue
            if isinstance(connection.input,Connection):
                continue
                        
            # Get start end end
            start = connection
            invert = connection.invert
            end = connection.output            
            while(end.output != None and isinstance(end.output,Connection)):
                self.connectionList.remove(end)
                if(end.invert == True):
                	invert = not invert                	
                end = end.output
                removedSize += 1
            
            # Output case, remove start and bind end to start input
            if(end.output == None and start.input == None):
                self.outputList[self.outputList.index(end)] = start
                start.input = None
                start.output = None
                removedSize += 1
            elif(end.output == None):
                end.input = None
                end.setInput(start.input)
                start.input.removeOutput(start)
                self.connectionList.remove(start)
                removedSize += 1
            # Input and connection case            
            else:
                start.output = None
                start.setOutput(end.output)
                end.output.removeInput(end)
                self.connectionList.remove(end)
                removedSize += 1
                
        return removedSize
                            
    def reduceMultioutputs(self):
        removedSize = 0
        
        for gate in list(self.gateList):
            if not isinstance(gate,Multioutput):
                continue
            
            # Input multioutput, leave it alone
            if(gate.inputList[0].input == None):
                continue
            
            inputGate = gate.inputList[0].input
            inputGate.removeOutput(gate.inputList[0])
            
            for outputConn in gate.outputList:
                outputConn.input = None
                outputConn.setInput(inputGate)
                
            self.gateList.remove(gate)
            removedSize += 1
            
        return removedSize
            


